﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using Microsoft.Office.Interop.Excel;
using Microsoft.SmallBasic.Library;

namespace Excel4SmallBasic
{
#if LANG_JA
    /// <summary>
    /// Excelオブジェクトは、Excelファイルのセルの読み書きの機能を提供します。
    /// </summary>
#else
    /// <summary>
    /// The Excel object provides methods to read and write value from and to a cell 
    /// of the Excel workbook.
    /// </summary>
#endif
    [SmallBasicType]
    public static class Excel
    {
        /// <summary>
        /// The upper bound of book cache size.
        /// いちどに開くことのできる Excel ファイル(ブック)の最大数です。
        /// </summary>
        private const int _MaxBookCache = 3;

        /// <summary>
        /// Excelアプリケーションを指すCOMインスタンスです。
        /// </summary>
        private static ApplicationClass _ExcelApp;

        /// <summary>アプリケーションドメインの終了イベントハンドラを登録済みかどうかを示します。</summary>
        private static bool _HookedProcessExitEvenet = false;

        /// <summary>
        /// Return Excel application object.
        /// Excelアプリケーションを返します。
        /// </summary>
        private static ApplicationClass GetExcelApp()
        {
            if (_ExcelApp == null)
            {
                _ExcelApp = new ApplicationClass();
                _ExcelApp.Visible = true;
            }
            if (_HookedProcessExitEvenet == false)
            {
                AppDomain.CurrentDomain.ProcessExit += new EventHandler(CurrentDomain_ProcessExit);
                _HookedProcessExitEvenet = true;
            }
            return _ExcelApp;
        }

        /// <summary>
        /// Called when application exit.
        /// アプリケーションが終了するときに呼び出されます。
        /// 開いていたブックはすべて閉じます。
        /// </summary>
        private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
        {
            if (_OpenedBooks != null) _OpenedBooks.ForEach(CloseBook);
            if (_ExcelApp != null)
            {
                _ExcelApp.Quit();
                Marshal.ReleaseComObject(_ExcelApp);
            }
        }

        /// <summary>
        /// The cache of opened book (Key=the full path of book, Value=book object).
        /// 開いたExcelファイル(ブック)をキャッシュする辞書です。
        /// (Key=ブックのフルパス, Value=ブック)
        /// いちばん最近参照されたブックが0番目の要素になります。
        /// </summary>
        private static List<KeyValuePair<string, _Workbook>> _OpenedBooks;

        /// <summary>
        /// Return the book of specified full path from cache.
        /// キャッシュ優先で、指定されたフルパスの Excel ファイル(ブック)を開いて返します。
        /// _MaxBookCache を超えるファイルを開こうとした場合は、LRU方式でいちばん過去に参照されたきりのファイルから自動で閉じます。
        /// </summary>
        private static _Workbook GetWorkbook(string filePath)
        {
            filePath = filePath.ToLower();

            // キャッシュ中に、指定されたフルパスのブックがあるか探索。
            if (_OpenedBooks == null) _OpenedBooks = new List<KeyValuePair<string, _Workbook>>();
            var openedBook = _OpenedBooks.FirstOrDefault(b => b.Key == filePath);
            
            // キャッシュ中になかった場合...
            if (openedBook.Key == null)
            {
                var app = GetExcelApp();
                var workBook = app.Workbooks.Open(filePath) as _Workbook;
                openedBook = new KeyValuePair<string, _Workbook>(filePath, workBook);
                _OpenedBooks.Insert(0, openedBook);

                // LRU方式で、いちばん過去に参照されたきりのファイルを閉じる(変更は上書き保存)。
                _OpenedBooks.Skip(_MaxBookCache).ToList().ForEach(CloseBook);
                while (_OpenedBooks.Count > _MaxBookCache) _OpenedBooks.RemoveAt(_OpenedBooks.Count - 1);

                // Excel画面中、ウィンドウをタイル表示する。
                app.Windows.Arrange(XlArrangeStyle.xlArrangeStyleTiled);
            }
            
            // キャッシュ中にみつかった場合は、キャッシュの辞書中、
            // 今回指定されたフルパスのブックを上位に移動する。
            else
            {
                _OpenedBooks.Remove(openedBook);
                _OpenedBooks.Insert(0, openedBook);
            }

            // このブックをアクティブにしてから返す。
            openedBook.Value.Activate();
            return openedBook.Value;
        }

        /// <summary>
        /// The action of closing book.
        /// ブックを閉じるアクションです。
        /// </summary>
        private static void CloseBook(KeyValuePair<string, _Workbook> book)
        {
            Console.WriteLine(book.Key);
            book.Value.Close(true);
            Marshal.ReleaseComObject(book.Value);
        }

#if LANG_JA
        /// <summary>
        /// <para>指定した Excel ファイルを開いて、指定のシート・セルの内容を読み取ります。</para>
        /// <para>同時に開くことができる Excel ファイルは3つまでです。</para>
        /// <para>4つ目を開くときに、最近最も読み書きされなかったファイルが、変更がある場合は上書き保存して自動で閉じられます。</para>
        /// </summary>
        /// <param name="filePath">セルの内容を読み取る Excel ファイルへの完全パスです。
        /// 完全パスはたとえば c:\tem\book1.xlsx のような形式です。</param>
        /// <param name="sheetName">読み取るセルのあるシートの名前です。</param>
        /// <param name="rowIndex">読み取るセルの行番号(1～)です。</param>
        /// <param name="columnIndex">読み取るセルの列番号(1～または"A","B",..."AB",...)です。</param>
        /// <returns>読み取ったセルの値です。</returns>
#else
        /// <summary>
        /// <para>Open the specified Excel file and reads the contents at the specified sheet,
        /// row and column number.</para>
        /// <para>When you attempts to open more than 3 files simultaneously,
        /// the least recently used one is closed automaticaly with saving chnages.</para>
        /// </summary>
        /// <param name="filePath">The full path of the Excel file to read from.
        /// An example of a full path will be c:\temp\book1.xlsx.</param>
        /// <param name="sheetName">The sheet name which contains the cell to be read.</param>
        /// <param name="rowIndex">The line number of the cell to be read.</param>
        /// <param name="columnIndex">The column number of the cell to be read.(ex. 1, 2,... or "A","B",..."AB",...)</param>
        /// <returns>The contents at the specified row, column, sheet of the specified Excel file.</returns>
#endif
        public static Primitive ReadCell(Primitive filePath, Primitive sheetName, Primitive rowIndex, Primitive columnIndex)
        {
            var workBook = GetWorkbook(filePath);
            
            var sheet = workBook.Worksheets[sheetName.ToString()] as _Worksheet;
            sheet.Activate();

            var cell = sheet.Cells[(int)rowIndex, ColIndexToInt(columnIndex)] as Range;
            cell.Activate();
            var rawValue = cell.Value;
            var value = rawValue != null ? rawValue.ToString() : string.Empty;

            Marshal.ReleaseComObject(cell);
            Marshal.ReleaseComObject(sheet);

            return value;
        }

#if LANG_JA
        /// <summary>
        /// <para>指定した Excel ファイルを開いて、指定のシート・セルに書き込みます。</para>
        /// <para>同時に開くことができる Excel ファイルは3つまでです。</para>
        /// <para>4つ目を開くときに、最近最も読み書きされなかったファイルが、変更がある場合は上書き保存して自動で閉じられます。</para>
        /// </summary>
        /// <param name="filePath">セルに書き込む Excel ファイルへの完全パスです。
        /// 完全パスはたとえば c:\tem\book1.xlsx のような形式です。</param>
        /// <param name="sheetName">書き込むセルのあるシートの名前です。</param>
        /// <param name="rowIndex">書き込むセルの行番号(1～)です。</param>
        /// <param name="columnIndex">書き込むセルの列番号(1～または"A","B",..."AB",...)です。</param>
        /// <param name="newValue">セルに書き込む内容です。</param>
#else
        /// <summary>
        /// <para>Open the specified Excel file and write the contents at the specified sheet,
        /// row and column number.</para>
        /// <para>When you attempts to open more than 3 files simultaneously,
        /// the least recently used one is closed automaticaly with saving chnages.</para>
        /// </summary>
        /// <param name="filePath">The full path of the Excel file to write to.
        /// An example of a full path will be c:\temp\book1.xlsx.</param>
        /// <param name="sheetName">The sheet name which contains the cell to write.</param>
        /// <param name="rowIndex">The line number of the cell to write.</param>
        /// <param name="columnIndex">The column number of the cell to write.(ex. 1, 2,... or "A","B",..."AB",...)</param>
        /// <param name="newValue">The contents to write at the specified row, column, sheet of the specified Excel file.</param>
#endif
        public static void WriteCell(Primitive filePath, Primitive sheetName, Primitive rowIndex, Primitive columnIndex, Primitive newValue)
        {
            var workBook = GetWorkbook(filePath);

            var sheet = workBook.Worksheets[sheetName.ToString()] as _Worksheet;
            sheet.Activate();

            var cell = sheet.Cells[(int)rowIndex, ColIndexToInt(columnIndex)] as Range;
            cell.Activate();
            cell.Value = newValue.ToString();

            Marshal.ReleaseComObject(cell);
            Marshal.ReleaseComObject(sheet);
        }

#if LANG_JA
        /// <summary>
        /// それまでに開いた Excel ファイルをすべて閉じます。
        /// セルの内容が変更されている Excel ファイルは上書き保存されます。
        /// </summary>
#else
        /// <summary>
        /// Close all Excel files which is opened with saving changes.
        /// </summary>
#endif
        public static void CloseAll()
        {
            if (_OpenedBooks != null)
            {
                _OpenedBooks.ForEach(CloseBook);
                _OpenedBooks.Clear();
            }
            if (_ExcelApp != null)
            {
                _ExcelApp.Quit();
                Marshal.ReleaseComObject(_ExcelApp);
                _ExcelApp = null;
            }
        }

        private static int ColIndexToInt(Primitive colIndex)
        {
            var colIdxStr = colIndex.ToString().ToUpper();
            if (Regex.IsMatch(colIdxStr, "^[A-Z]+$"))
            {
                const int radix = 26;
                var baseCharCode = Convert.ToInt32('A');
                return colIdxStr
                    .Select(c => Convert.ToInt32(c) - baseCharCode + 1)
                    .Aggregate((a, b) => a * radix + b);
            }
            else return (int)colIndex;
        }
    }
}
